--kopisusu.common
--[[
MIT License

Copyright (c) 2023 Anggara Yama Putra

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--]]
-- Globals
AnggaraNothing = AnggaraNothing or {}
do
end

local HANDLED = AnggaraNothing.sentinel('return_handled')
local OVERRIDDEN = AnggaraNothing.sentinel('return_overridden')

-- module: KopiSusu
do
  local root = {}

  -- This function handles the event.
  root.handled = function (...) return HANDLED, ... end
  -- This function overrides the call arguments.
  root.overridden = function (...) return OVERRIDDEN, ... end

  AnggaraNothing.KopiSusu = root
end

-- class: KopiSusu.Chain
do
  local root = {}

  local strformat = string.format
  local tablepack = table.pack
  local tableunpack = table.unpack
  local tableinsert = table.insert
  local tabledefaults = AnggaraNothing.table_defaults

  local STATES = {
    [HANDLED] = 1<<0,
    [OVERRIDDEN] = 1<<1,
  }
  local function parse (...)
    local bitfield = 0
    local start = 1
    for i = start, select('#', ...) do
      local curstate = STATES[select(i, ...)]
      if curstate then
        bitfield = bitfield | curstate
      else
        start = i
        break
      end
    end
    return bitfield, tablepack(select(start, ...))
  end

  -- @param returns_mapping Returns mapping table. Format: {[return index] = [argument index], ...}
  function root:Create (name, returns_mapping)
    local instance = {
      m_len = 0,
      m_funcs_idx = setmetatable({}, {
        __mode = 'k',
      }),
      m_returns_mapping = returns_mapping,
    }

    local mt = root.m_metatable
    setmetatable(instance, tabledefaults(
      {
        __name = strformat( '%s(%s)', mt.__name, name ),
        __call = function (_, ...)
          return instance:Run(...)
        end,
      }
    , mt))
    return instance
  end

  do
    local broken_idx_err = 'broken idx hashmap at %d'
    -- @return index of `func`.
    function root:Append (func)
      local idx = self.m_funcs_idx
      local id = idx[func]
      if id then
        assert( self[id] == func, strformat(broken_idx_err, id) )
        return id
      end
      id = #self + 1
      self[id] = func
      idx[func] = id
      self.m_len = self.m_len + 1

      self:OnPostAdd( func )

      return id
    end

    -- @return index of `func`.
    function root:Insert (position, func)
      local idx = self.m_funcs_idx
      local id = idx[func]
      if id then
        assert( self[id] == func, strformat(broken_idx_err, id) )
        return id
      end
      id = position
      local slot = self[id]
      if slot == false then
        -- Empty slot.
        self[id] = func
      else
        -- New slot insertion.
        tableinsert( self, id, func )
        -- Fix hashmap.
        for k,i in pairs(idx) do
          if i >= id then
            idx[k] = i + 1
          end
        end
      end
      idx[func] = id
      self.m_len = self.m_len + 1

      self:OnPostAdd( func )

      return id
    end

    -- Remove `func` from list.
    --
    -- @return boolean
    function root:Delete (func)
      local idx = self.m_funcs_idx
      local id = idx[func]
      if not id then
        return false
      end
      assert( self[id] == func, strformat(broken_idx_err, id) )
      self:OnPreDelete( func )
      self[id] = false -- nil value would make table holes and screw up the table length
      idx[func] = nil
      self.m_len = self.m_len - 1
      return true
    end
  end

  -- @return index of `func`.
  function root:IndexOf (func)
    return self.m_funcs_idx[func]
  end

  function root:Length ()
    return self.m_len
  end

  -- Remove all functions from list.
  function root:Clear ()
    self:OnPreClear()

    local del = self.Delete
    for k,_ in pairs(self.m_funcs_idx) do
      del( self, k )
    end
    assert( self.m_len == 0, strformat('m_len is non-zero %d', self.m_len) )
    for i = 1, #self do
      self[i] = nil
    end
    assert( #self == 0, strformat('#self is non-zero %d', #self) )
  end

  function root:RunAt (start, ...)
    start = start or 1
    local varargs = tablepack( ... )
    local varargs_len = varargs.n
    local funcs_len = #self
    local argsmap = self.m_returns_mapping
    local rets, rets_len
    for i = start, funcs_len do
      local func = self[i]
      if func then
        func = self:OnPreRun(func) or func
        local states, currets = parse( func(tableunpack(varargs, 1, varargs_len)) )
        local currets_len = currets.n
        if currets_len > 0 then
          rets, rets_len = currets, currets_len
        end
        if states & STATES[HANDLED] ~= 0 then
          -- Handled by current func and stop the loop.
          break
        elseif argsmap and i ~= funcs_len and states & STATES[OVERRIDDEN] ~= 0 then
          -- Returns mapping for varargs
          for j = 1, currets_len do
            local arg_pos = argsmap[j]
            if arg_pos then
              varargs[arg_pos] = currets[j]
            end
          end
        end
      end
    end
    if rets_len and rets_len > 0 then
      assert( rets, 'rets is nil' )
      return tableunpack( rets, 1, rets_len )
    end
  end

  function root:Run (...)
    return self:RunAt(nil, ...)
  end

  -- Events
  do
    local dprint = log or print

    function root:OnPostAdd (func)
      dprint(strformat(
        '%s::OnPostAdd::%s',
        self, func
      ))
    end
    function root:OnPreDelete (func)
      dprint(strformat(
        '%s::OnPreDelete::%s',
        self, func
      ))
    end
    function root:OnPreClear ()
    end
    function root:OnPreRun (func)
    end
  end

  local metatable = {}
  metatable.__index = root
  metatable.__name = 'KopiSusu::Chain'
  metatable.__type = 'KopiSusu::Chain'

  root.m_metatable = metatable
  AnggaraNothing.KopiSusu.Chain = root
end

-- module: KopiSusu
do
  local root = AnggaraNothing.KopiSusu

  local strformat = string.format
  local tableunpack = table.unpack
  local tabledefaults = AnggaraNothing.table_defaults
  local Chain = root.Chain

  local mt_name = 'KopiSusu::Proxy'
  local default_mt = {
    __index = function (t, k)
      local tmp = t[2][k]
      if tmp then
        return tmp
      end
      return t[1][k]
    end,
    __newindex = function (t, k, v)
      local tmp = t[2][k]
      if tmp then
        tmp:Append( v )
        return
      end
      t[1][k] = v
    end,
    __type = mt_name,
  }

  -- @param args Format: {{[name], [returns mapping table]}, ...}
  --
  -- @return an proxy table.
  function root:CreateProxy (source, args)
    local result = {source, {},}

    local events = result[2]
    for i=1, #args do
      local name, returns_mapping = tableunpack( args[i] )
      if not events[name] then
        local evt = Chain:Create( name, returns_mapping )
        events[name] = evt
        source[name] = evt
      end
    end

    local result_mt = tabledefaults(
      {
        __name = strformat('%s(%s)', mt_name, source),
      }
    , default_mt)

    return setmetatable(result, result_mt)
  end
end

--[[ tests
do
  local tableconcat = table.concat
  local tablepack = table.pack
  local dprint = log or print
  local root = AnggaraNothing.KopiSusu
  local Chain = root.Chain

  local tmp = {}
  local zero_return_factory = function ()return function()end end
  local zero_return = function ()end
  local func_a = function () return 'func_a->' end
  local func_b = function (...) return 'func_b->' end

  local function basic (sample)
    dprint( sample )
    -- 1 func
    sample:Append( zero_return )
    assert( sample:IndexOf(zero_return), '[zero_return] failed at Append' )
    assert( sample() == nil, '[zero_return] failed at Run' )
    assert( sample(1,2,3) == nil, '[zero_return] failed at Run' )
    -- 2 funcs
    sample:Append( func_a )
    assert( sample:IndexOf(func_a), '[func_a] failed at Append' )
    local returns = tablepack( sample() )
    dprint( tableconcat(returns) )
    assert( returns[1] == 'func_a->', '[func_a] failed at Run' )
    returns = tablepack( sample(1,2,3) )
    dprint( tableconcat(returns) )
    assert( returns[1] == 'func_a->', '[func_a] failed at Run' )
    assert( sample:Append(zero_return), '[no duplicate] failed at Append' )
    assert( sample:Length() == 2, '[no duplicate] failed at Append' )
    assert( sample:Delete(zero_return), '[zero_return] failed at Delete' )
    assert( sample:IndexOf(zero_return) == nil, '[zero_return] failed at Delete' )
    sample:Append( zero_return )
    assert( sample:IndexOf(zero_return), '[2 funcs] failed at Append' )
    returns = tablepack( sample() )
    dprint( tableconcat(returns) )
    assert( returns[1] == 'func_a->', '[2 funcs] failed at Run' )
    returns = tablepack( sample(1,2,3) )
    dprint( tableconcat(returns) )
    assert( returns[1] == 'func_a->', '[2 funcs] failed at Run' )
    -- 3 funcs
    sample:Append( func_b )
    returns = tablepack( sample() )
    dprint( tableconcat(returns) )
    assert( returns[1] == 'func_b->', '[3 funcs] failed at Run' )
    returns = tablepack( sample(1,2,3) )
    dprint( tableconcat(returns) )
    assert( returns[1] == 'func_b->', '[3 funcs] failed at Run' )
    sample:Delete( func_b )
    returns = tablepack( sample() )
    dprint( tableconcat(returns) )
    assert( returns[1] == 'func_a->', '[del func_b] failed at Run' )
    returns = tablepack( sample(1,2,3) )
    dprint( tableconcat(returns) )
    assert( returns[1] == 'func_a->', '[del func_b] failed at Run' )
    sample:Append( func_b )
    -- 4 funcs
    sample:Append( zero_return_factory() )
    dprint( sample:Length() )
    assert( sample:Length() == 4, '[zero_return_factory] failed at Append' )
    assert( sample() == 'func_b->', '[zero_return_factory] failed at Run' )
    assert( sample(1,2,3) == 'func_b->', '[zero_return_factory] failed at Run' )
    -- 0 func
    sample:Clear()
    for i = 1, 4 do tmp[i] = tostring(sample[i]) end
    dprint( sample:Length(), ' ', tableconcat(tmp, ',') )
    assert( sample:Length() == 0, '[0 func] failed at Clear' )
    assert( sample.m_funcs_idx[zero_return] == nil, '[0 func] failed at Clear' )
    assert( sample() == nil, '[0 func] failed at Run' )
    assert( sample(1,2,3) == nil, '[0 func] failed at Run' )
    -- 3 funcs
    sample:Append( zero_return )
    sample:Append( func_a )
    sample:Append( zero_return )
    sample:Append( func_b )
    dprint( sample:Length() )
    assert( sample:Length() == 3, '[refill] failed at Append' )
    assert( sample() == 'func_b->', '[refill] failed at Run' )
    assert( sample(1,2,3) == 'func_b->', '[refill] failed at Run' )
  end
  basic( Chain:Create('no_mapping') )
end
--]]
